import datetime
import importlib
import inspect
import os
import re
import sys
from collections import Mapping
from datetime import timedelta
from json import JSONEncoder
from typing import Any, List
from unicodedata import normalize

import simplejson
from flask_sqlalchemy import model
from marshmallow import Schema

# from app.utils.webargs import use_kwargs as base_use_kwargs, parser
from app.models.bo.base_bo import BaseBO
from app.models.po.base_po import BasePO, NewBasePO


class ParamsDict(dict):
    """Just available update func.

    Example::

        @use_kwargs(PageParams.update({...}))
        def list_users(page, page_size, order_by):
            pass

    """

    def update(self, other=None):
        """Update self by other Mapping and return self.
        """
        ret = ParamsDict(self.copy())
        if other is not None:
            for k, v in other.items() if isinstance(other, Mapping) else other:
                ret[k] = v
        return ret


class dict2object(dict):
    """Dict to fake object that can use getattr.

    Examples::

        In [2]: obj = dict2object({'a': 2, 'c': 3})

        In [3]: obj.a
        Out[3]: 2

        In [4]: obj.c
        Out[4]: 3

    """

    def __getattr__(self, name: str) -> Any:
        if name in self.keys():
            return self[name]
        raise AttributeError('object has no attribute {}'.format(name))

    def __setattr__(self, name: str, value: Any) -> None:
        if not isinstance(name, str):
            raise TypeError('key must be string type.')
        self[name] = value


def secure_filename(filename: str) -> str:
    """Borrowed from werkzeug.utils.secure_filename.

    Pass it a filename and it will return a secure version of it. This
    filename can then safely be stored on a regular file system and passed
    to :func:`os.path.join`.

    On windows systems the function also makes sure that the file is not
    named after one of the special device files.

        >>> secure_filename(u'哈哈.zip')
        '哈哈.zip'
        >>> secure_filename('My cool movie.mov')
        'My_cool_movie.mov'
        >>> secure_filename('../../../etc/passwd')
        'etc_passwd'
        >>> secure_filename(u'i contain cool \xfcml\xe4uts.txt')
        'i_contain_cool_umlauts.txt'

    """

    for sep in os.path.sep, os.path.altsep:
        if sep:
            filename = filename.replace(sep, ' ')

    filename = normalize('NFKD', '_'.join(filename.split()))

    filename_strip_re = re.compile(u'[^A-Za-z0-9\u4e00-\u9fa5_.-]')
    filename = filename_strip_re.sub('', filename).strip('._')

    # on nt a couple of special files are present in each folder.  We
    # have to ensure that the target file is not such a filename.  In
    # this case we prepend an underline
    windows_device_files = (
        'CON', 'AUX', 'COM1', 'COM2', 'COM3', 'COM4', 'LPT1',
        'LPT2', 'LPT3', 'PRN', 'NUL',
    )
    if os.name == 'nt' and filename and \
            filename.split('.')[0].upper() in windows_device_files:
        filename = '_' + filename

    return filename


def _get_init_args(instance, base_class):
    """Get instance's __init__ args and it's value when __call__.
    """
    getargspec = inspect.getfullargspec

    argspec = getargspec(base_class.__init__)
    no_defaults = argspec.args[:-len(argspec.defaults)]
    has_defaults = argspec.args[-len(argspec.defaults):]

    kwargs = {k: getattr(instance, k) for k in no_defaults
              if k != 'self' and hasattr(instance, k)}
    kwargs.update({k: getattr(instance, k) if hasattr(instance, k) else
    getattr(instance, k, argspec.defaults[i])
                   for i, k in enumerate(has_defaults)})

    assert len(kwargs) == len(argspec.args) - 1, 'exclude `self`'

    return kwargs


# def use_kwargs(argmap, schema_kwargs: Optional[Dict] = None, **kwargs: Any):
#     """For fix ``Schema(partial=True)`` not work when used with
#     ``@webargs.flaskparser.use_kwargs``. More details ``see webargs.core``.
#
#     Args:
#
#         argmap (marshmallow.Schema,dict,callable): Either a
#             `marshmallow.Schema`, `dict` of argname ->
#             `marshmallow.fields.Field` pairs, or a callable that returns a
#             `marshmallow.Schema` instance.
#         schema_kwargs (dict): kwargs for argmap.
#
#     Returns:
#         dict: A dictionary of parsed arguments.
#
#     """
#     schema_kwargs = schema_kwargs or {}
#
#     argmap = parser._get_schema(argmap, request)
#
#     if not (argmap.partial or schema_kwargs.get('partial')):
#         return base_use_kwargs(argmap, **kwargs)
#
#     def factory(request):
#         argmap_kwargs = _get_init_args(argmap, Schema)
#         argmap_kwargs.update(schema_kwargs)
#
#         # force set force_all=False
#         only = parser.parse(argmap, request).keys()
#
#         argmap_kwargs.update({
#             'partial': False,  # fix missing=None not work
#             'only': only or None,
#             'context': {"request": request},
#         })
#         if tuple(LooseVersion(marshmallow.__version__).version)[0] < 3:
#             argmap_kwargs['strict'] = True
#
#         return argmap.__class__(**argmap_kwargs)
#
#     return base_use_kwargs(factory, **kwargs)
#

def import_subs(locals_, modules_only: bool = False) -> List[str]:
    """ Auto import submodules, used in __init__.py.

    Args:

        locals_: `locals()`.
        modules_only: Only collect modules to __all__.

    Examples::

        # app/models/__init__.py
        from core.utils import import_subs

        __all__ = import_subs(locals())

    Auto collect Model's subclass, Schema's subclass and instance.
    Others objects must defined in submodule.__all__.
    """
    package = locals_['__package__']
    path = locals_['__path__']
    top_mudule = sys.modules[package]

    all_ = []
    for name in os.listdir(path[0]):
        if not name.endswith(('.py', '.pyc')) or name.startswith('__init__.'):
            continue

        module_name = name.split('.')[0]
        submodule = importlib.import_module(f".{module_name}", package)
        all_.append(module_name)

        if modules_only:
            continue

        if hasattr(submodule, '__all__'):
            for name in getattr(submodule, '__all__'):
                if not isinstance(name, str):
                    raise Exception(f'Invalid object {name} in __all__, '
                                    f'must contain only strings.')
                setattr(top_mudule, name, getattr(submodule, name))
                all_.append(name)
        else:
            for name, obj in submodule.__dict__.items():
                if isinstance(obj, (model.DefaultMeta, Schema)) or \
                        (inspect.isclass(obj) and
                         (issubclass(obj, Schema) or
                          obj.__name__.endswith('Service'))):
                    setattr(top_mudule, name, obj)
                    all_.append(name)
    return all_


def copy_properties(origin, dest):
    if dest is None or origin is None:
        return None
    if isinstance(origin, dict):
        attrs = origin.keys()
        for field in attrs:
            if hasattr(dest, field):
                data = origin.get(field)
                dest.__setattr__(field, data)
    else:
        attrs = [x for x in dir(origin)]
        for field in attrs:
            if not field.startswith('_'):
                if hasattr(dest, field):
                    data = origin.__getattribute__(field)
                    dest.__setattr__(field, data)


class DatetimeJsonEncoder(simplejson.JSONEncoder):
    def default(self, o):
        if isinstance(o, datetime.datetime):
            return o.strftime("%Y-%m-%d %H:%M:%S")


def get_time_round_and_adjust(dt: datetime.datetime, step=0, round_level="s"):
    """
    计算整分钟，整小时，整天的时间
    :param step: 往前或往后跳跃取整值，默认为0，即当前所在的时间，正数为往后，负数往前。
                例如：
                step = 0 时 2019-04-11 17:38:21.869993 取整秒后为 2019-04-11 17:38:21
                step = 1 时 2019-04-11 17:38:21.869993 取整秒后为 2019-04-11 17:38:22
                step = -1 时 2019-04-11 17:38:21.869993 取整秒后为 2019-04-11 17:38:20
    :param round_level: 字符串格式。
                "s": 按秒取整；"min": 按分钟取整；"hour": 按小时取整；"days": 按天取整
    :return: 整理后的时间戳
    """
    if round_level == "days":  # 整天
        td = timedelta(days=-step, seconds=dt.second, microseconds=dt.microsecond, milliseconds=0, minutes=dt.minute,
                       hours=dt.hour, weeks=0)
        new_dt = dt - td
    elif round_level == "hour":  # 整小时
        td = timedelta(days=0, seconds=dt.second, microseconds=dt.microsecond, milliseconds=0, minutes=dt.minute,
                       hours=-step, weeks=0)
        new_dt = dt - td
    elif round_level == "min":  # 整分钟
        td = timedelta(days=0, seconds=dt.second, microseconds=dt.microsecond, milliseconds=0, minutes=-step, hours=0,
                       weeks=0)
        new_dt = dt - td
    elif round_level == "s":  # 整秒
        td = timedelta(days=0, seconds=-step, microseconds=dt.microsecond, milliseconds=0, minutes=0, hours=0, weeks=0)
        new_dt = dt - td
    else:
        new_dt = dt
    return new_dt


def get_list_intersect(list1, list2):
    return list(set(list1).intersection(set(list2)))


class GaoLuEncoder(JSONEncoder):
    def default(self, o):
        if isinstance(o, BasePO) or isinstance(o, BaseBO) or isinstance(o,NewBasePO):
            exclude = [i for i in o.__dir__() if i.startswith('_')] + ['metadata', 'query', 'query_class']
            ret_dict = o.__dict__
            for item in exclude:
                ret_dict.pop(item, None)
            for k, v in ret_dict.items():
                if isinstance(v, datetime.datetime):
                    ret_dict[k] = v.strftime("%Y-%m-%d %H:%M:%S")
                if isinstance(v, datetime.date):
                    ret_dict[k] = str(v)
            return ret_dict
        if isinstance(o, datetime.datetime):
            return o.strftime("%Y-%m-%d %H:%M:%S")


def string_to_num(s: str):
    """Converts a string to an int/float

    Returns ``None`` if it can't be converted

    >>> string_to_num('5')
    5
    >>> string_to_num('5.2')
    5.2
    >>> string_to_num(10)
    10
    >>> string_to_num(10.1)
    10.1
    >>> string_to_num('this is not a string') is None
    True
    """
    if isinstance(s, (int, float)):
        return s
    if s.isdigit():
        return int(s)
    try:
        return float(s)
    except ValueError:
        return None


def string_to_bool(s: str):
    if isinstance(s, (bool,)):
        return s
    elif s.lower() == 'true':
        return True
    return False

